package org.third.integration.ldap;

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.SpringSecurityMessageSource;

public class LdapUtil {

	/** Logging support */
	private static final Logger logger = LoggerFactory.getLogger(LdapUtil.class);

	/** UTF-8 character set */
	private static final Charset UTF8 = Charset.forName("UTF-8");
	/** Source for localizable LDAP error messages */
	private static final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	/** Regular expression for parsing LDAP error codes */
	private static final Pattern SUB_ERROR_CODE = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*");

	/**
	 * LDAP error code issued from authentication operation when username is not
	 * provided
	 */
	private static final int USERNAME_NOT_FOUND = 0x525;
	/** LDAP error code issued from authentication operation for invalid password */
	private static final int INVALID_PASSWORD = 0x52e;
	/**
	 * LDAP error code issued when the operation is not permitted (insufficient
	 * authorization)
	 */
	private static final int NOT_PERMITTED = 0x530;
	/**
	 * LDAP error code issued when the user cannot be authenticated due to an
	 * expired password
	 */
	private static final int PASSWORD_EXPIRED = 0x532;
	/**
	 * LDAP error code issued when the user cannot be authenticated due to an
	 * disabled account
	 */
	private static final int ACCOUNT_DISABLED = 0x533;
	/**
	 * LDAP error code issued when the user cannot be authenticated due to an
	 * expired account
	 */
	private static final int ACCOUNT_EXPIRED = 0x701;
	/**
	 * LDAP error code issued when the user cannot be authenticated because the
	 * password needs to be reset
	 */
	private static final int PASSWORD_NEEDS_RESET = 0x773;
	/**
	 * LDAP error code issued when the user cannot be authenticated due to a locked
	 * account
	 */
	private static final int ACCOUNT_LOCKED = 0x775;
	/**
	 * Judges if one string is digital.
	 * 
	 * @param strValue
	 *            boolean value
	 * @return return true if strValue is digital. otherwise it will retured as
	 *         false.
	 */
	public static final Pattern NUMERIC_PATTERN = Pattern.compile("^[-\\+]?[.\\d]*$");

	public static boolean isNumeric(String strValue) {
		return NUMERIC_PATTERN.matcher(strValue).matches();
	}

	/**
	 * Removes extraneous whitespace and sets the case of all DN component types to
	 * lowercase.
	 *
	 * @param dn
	 *            the distinguished name to normalize
	 *
	 * @return the normalized distinguished name, or the input if an error occurred.
	 */
	public static String normalizeDn(String dn) {
		LdapName ldapDn;
		try {
			ldapDn = new LdapName(dn);
		} catch (NamingException ex) {
			logger.error("Could not process LDAP DN: " + ex.getMessage(), ex);
			return dn;
		}

		boolean first = true;
		StringBuilder builder = new StringBuilder();
		List<Rdn> components = ldapDn.getRdns();
		ListIterator<Rdn> iter = components.listIterator(components.size());
		while (iter.hasPrevious()) {
			Rdn component = iter.previous();
			String type = component.getType();
			Object value = component.getValue();
			String valueText = (value != null ? Rdn.escapeValue(value) : "");
			if (!first) {
				builder.append(",");
			}
			builder.append(type.toLowerCase()).append("=").append(valueText);
			first = false;
		}
		return builder.toString();
	}

	/**
	 * Constructs a
	 * {@link org.springframework.security.authentication.BadCredentialsException}
	 *
	 * @return the new
	 *         {@link org.springframework.security.authentication.BadCredentialsException}
	 */
	public static BadCredentialsException badCredentials() {
		return new BadCredentialsException(
				messages.getMessage("LdapAuthenticationProvider.badCredentials", "Bad credentials"));
	}

	/**
	 * Constructs a {@link BadCredentialsException} with the given cause
	 *
	 * @param cause
	 *            the exception that caused the {@link BadCredentialsException}
	 *
	 * @return the new {@link BadCredentialsException}
	 */
	@SuppressWarnings("all")
	public static BadCredentialsException badCredentials(Throwable cause) {
		return (BadCredentialsException) badCredentials().initCause(cause);
	}

	/**
	 * Processes exceptions that occurred during the bind operation.
	 *
	 * @param bindPrincipal
	 *            information about the user for whom authentication failed
	 * @param exception
	 *            the exception thrown indicating authentication failure
	 */
	public static void handleBindException(String bindPrincipal, NamingException exception) {
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
		}

		int subErrorCode = parseSubErrorCode(exception.getMessage());

		if (subErrorCode <= 0) {
			logger.debug("Failed to locate AD-specific sub-error code in message");
			return;
		}

		logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));

		// if(convertSubErrorCodesToExceptions) {
		// raiseExceptionForErrorCode(subErrorCode, exception);
		// }
	}

	/**
	 * Parses the LDAP error code associated with an LDAP exception
	 *
	 * @param message
	 *            the exception message
	 *
	 * @return the LDAP error code embedded in the message text
	 */
	public static int parseSubErrorCode(String message) {
		Matcher m = SUB_ERROR_CODE.matcher(message);

		if (m.matches()) {
			return Integer.parseInt(m.group(1), 16);
		}

		return -1;
	}

	/**
	 * Translates an LDAP error code to an appropriate log message
	 *
	 * @param code
	 *            the LDAP error code
	 *
	 * @return the log message text
	 */
	public static String subCodeToLogMessage(int code) {
		switch (code) {
		case USERNAME_NOT_FOUND:
			return "User was not found in directory";
		case INVALID_PASSWORD:
			return "Supplied password was invalid";
		case NOT_PERMITTED:
			return "User not permitted to logon at this time";
		case PASSWORD_EXPIRED:
			return "Password has expired";
		case ACCOUNT_DISABLED:
			return "Account is disabled";
		case ACCOUNT_EXPIRED:
			return "Account expired";
		case PASSWORD_NEEDS_RESET:
			return "User must reset password";
		case ACCOUNT_LOCKED:
			return "Account locked";
		}

		return "Unknown (error code " + Integer.toHexString(code) + ")";
	}

	/**
	 * Computes a fully qualified distinguished name from a relative distinguished
	 * name, given the LDAP configuration.
	 *
	 * @param inputName
	 *            the input distinguished name, which may be relative or fully
	 *            qualified
	 * @param ldapConfig
	 *            the LDAP configuration, which includes the base DN for the LDAP
	 *            domain
	 *
	 * @return the fully qualified distinguished name
	 */
	public static LdapName completeLdapName(String inputName, String baseDn) {
		LdapName name;
		try {
			name = new LdapName(inputName != null ? inputName : "");
		} catch (InvalidNameException ex) {
			logger.error("Unable to convert \"" + inputName + "\" to an LDAP name");
			return null;
		}

		if (null != baseDn) {
			logger.debug("Base DN not provided with LDAP configuration");
			return name;
		}

		LdapName fullName;
		try {
			fullName = new LdapName(baseDn);
		} catch (InvalidNameException ex) {
			logger.error("Invalid base DN provided in LDAP configuration");
			return name;
		}

		if (name.startsWith(fullName)) {
			return name;
		} else {
			try {
				fullName.addAll(name);
				return fullName;
			} catch (InvalidNameException ex) {
				logger.error("Cannot construct full DN from relative DN");
				return name;
			}
		}
	}

	/**
	 * Completes the given LDAP name with the given LDAP base name
	 *
	 * @param inputName
	 *            the input name
	 * @param baseName
	 *            the base name
	 *
	 * @return the fully qualified distinguished name
	 */
	public static LdapName completeLdapName(String inputName, LdapName baseName) {
		LdapName name;
		try {
			name = new LdapName(inputName != null ? inputName : "");
		} catch (InvalidNameException ex) {
			logger.error("Unable to convert \"" + inputName + "\" to an LDAP name");
			return null;
		}

		if (baseName == null) {
			logger.debug("Base name not provided.");
			return name;
		}

		LdapName fullName;
		try {
			fullName = new LdapName(baseName.toString());
		} catch (InvalidNameException ex) {
			logger.error("Invalid base name");
			return name;
		}

		if (name.startsWith(fullName)) {
			return name;
		} else {
			try {
				fullName.addAll(name);
				return fullName;
			} catch (InvalidNameException ex) {
				logger.error("Cannot construct full DN from relative DN");
				return name;
			}
		}
	}

	public static String buildLdapQuery(String GroupMembershipAttribs) {
		String memberAttribNamesRaw = "member,uniqueMember";
		String[] memberAttribNames = memberAttribNamesRaw.split("\\s*,\\s*");

		StringBuilder builder = new StringBuilder("(|");
		for (String attrName : memberAttribNames) {
			builder.append("(").append(attrName).append("={0})");
		}
		builder.append(")");

		return builder.toString();
	}

	/**
	 * Gets values from the attributes of the search result
	 *
	 * @param searchResult
	 *            the searchResult object
	 * @return {@link Enumeration} the values from attributes
	 */
	public static Enumeration getValuesFromAttrs(SearchResult searchResult) {

		Enumeration vals = null;

		Attributes attrs = searchResult.getAttributes();

		if (null != attrs) {
			for (NamingEnumeration ae = attrs.getAll(); ae.hasMoreElements();) {
				Attribute atr = null;
				try {
					atr = (Attribute) ae.next();
				} catch (NamingException e) {
					logger.debug("No attribute found in the list of attributes");
				}

				if (atr != null) {
					String attributeID = atr.getID();

					try {
						vals = atr.getAll();
					} catch (NamingException e) {
						logger.debug("No values found in the attribute" + attributeID);
					}

				} else {
					logger.debug("No attribute found in the attributes list in search result for the group");
				}
			}
		} else {
			logger.debug("No attributes found in search result for the group");
		}
		return vals;
	}

	/**
	 * Gets values from the attributes of the search result in pagination
	 * 
	 * @param ctx
	 *            the LdapContext instance which delegate the ldap operation
	 * @param searchBase
	 *            which ldap object will be retrieved
	 * @param searchFilter
	 *            ldap search filter
	 * @param searchCtls
	 *            the SearchControls object
	 * @param startIndex
	 *            which item will be returned in this page
	 * @param endIndex
	 *            which last item will be returned in this page
	 * 
	 * @return {@link Enumeration} the values from attributes
	 * @throws NamingException
	 */
	public static List<Object> getValueListFromRangedAttrs(LdapContext ctx, String searchBase, String searchFilter,
			SearchControls searchCtls, int startIndex, int endIndex) throws NamingException {
		String rangeString = ";range=";
		List<Object> attributeValues = new ArrayList<>();
		boolean continueRetrieve = true;
		int page = 1;
		try {
			FIND_NEXT_PAGE: while (continueRetrieve) {

				String[] returningAttributes = searchCtls.getReturningAttributes();
				if (startIndex > 0 && endIndex > 0) {
					for (int i = 0, size = returningAttributes.length; i < size; i++) {
						String lowerAttr = returningAttributes[i].toLowerCase();
						String attr = returningAttributes[i];
						if (lowerAttr.contains(rangeString)) {
							int rangeStringIndex = lowerAttr.indexOf(rangeString);
							returningAttributes[i] = attr.substring(0, rangeStringIndex) + rangeString + startIndex
									+ "-" + endIndex;
						} else {
							returningAttributes[i] = returningAttributes[i] + rangeString + startIndex + "-" + endIndex;
						}
					}
				}
				NamingEnumeration<SearchResult> answer = ctx.search(searchBase, searchFilter, searchCtls);
				if (answer.hasMoreElements()) {
					SearchResult searchResult = answer.nextElement();
					Attributes attrs = searchResult.getAttributes();
					logger.trace("Find page = {}, startIndex={}, endIndex={}", page, startIndex, endIndex);
					if (null != attrs) {
						for (NamingEnumeration<? extends Attribute> ae = attrs.getAll(); ae.hasMoreElements();) {
							Attribute attr = ae.next();
							NamingEnumeration<?> attrValue = attr.getAll();
							boolean findAttribute = attrValue.hasMoreElements();
							while (attrValue.hasMoreElements()) {
								attributeValues.add(attrValue.next());
							}
							if (findAttribute) {

								startIndex = -1;
								endIndex = -1;
								String lowerAttrId = attr.getID().toLowerCase();
								if ((startIndex = lowerAttrId.indexOf(rangeString)) > 0) {
									startIndex = startIndex + rangeString.length();
									String str2 = attr.getID().substring(startIndex);
									String[] rangeIndexString = str2.split("-");
									startIndex = Integer.parseInt(rangeIndexString[0]);
									if (rangeIndexString.length < 2 || !isNumeric(rangeIndexString[1])) {
										continueRetrieve = false;
										break FIND_NEXT_PAGE;
									}
									endIndex = Integer.parseInt(rangeIndexString[1]);
								} else {
									continueRetrieve = false;
									break FIND_NEXT_PAGE;
								}
								int pageSize = endIndex - startIndex + 1;
								startIndex = endIndex + 1;
								endIndex = endIndex + pageSize;
							}
						}
					}
				} else {
					continueRetrieve = false;
					break FIND_NEXT_PAGE;
				}
				++page;
			}
		} catch (Exception e) {
			String errorInfo = "No attribute found in the list of attributes, searchBase=" + searchBase
					+ ", searchFilter=" + searchFilter;
			logger.error(errorInfo, e);
			throw new NamingException(errorInfo);
		}
		logger.trace("Find items = {}, page={}, startIndex={}, endIndex={}", attributeValues.size(), page, startIndex,
				endIndex);

		return attributeValues;
	}

	public static final void printSearchResult(NamingEnumeration<SearchResult> searchResults) throws NamingException {
		while (searchResults.hasMoreElements()) {
			SearchResult sr = (SearchResult) searchResults.next();
			System.out.println("SearchResult.next()<<<::[" + sr.getName() + "]::>>>>");
			System.out.println("getName():" + sr.getName());
			System.out.println("getNameInNamespace():" + sr.getNameInNamespace());
			javax.naming.directory.Attributes attrs = sr.getAttributes();
			if (attrs == null || attrs.size() == 0) {
				System.out.println("No   attributes");

			} else {
				for (javax.naming.NamingEnumeration<? extends javax.naming.directory.Attribute> attrEnum = sr
						.getAttributes().getAll(); attrEnum.hasMore();) {
					javax.naming.directory.Attribute attr = attrEnum.next();
					System.out.println(attr.getID() + "=" + attr.get());
				}
				javax.naming.directory.Attribute attr = attrs.get("userPassword");
				if (attr != null) {
					Object o = attr.get();
					byte[] s = (byte[]) o;
					String pwd2 = new String(s);
					String pwd = "1";
					try {
						System.out.println((verifySHA(pwd2, pwd)));
					} catch (NoSuchAlgorithmException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	public static boolean verifySHA(String ldappw, String inputpw) throws NoSuchAlgorithmException {

		// MessageDigest 提供了消息摘要算法，如 MD5 或 SHA，的功能，这里LDAP使用的是SHA-1
		MessageDigest md = MessageDigest.getInstance("SHA-1");

		// 取出加密字符
		if (ldappw.toUpperCase().startsWith("{SSHA}")) {
			ldappw = ldappw.substring(6);
		} else if (ldappw.toUpperCase().startsWith("{SHA}")) {
			ldappw = ldappw.substring(5);
		}

		// 解码BASE64
		byte[] ldappwbyte = java.util.Base64.getDecoder().decode(ldappw.getBytes());
		byte[] shacode;
		byte[] salt;

		// 前20位是SHA-1加密段，20位后是最初加密时的随机明文
		if (ldappwbyte.length <= 20) {
			shacode = ldappwbyte;
			salt = new byte[0];
		} else {
			shacode = new byte[20];
			salt = new byte[ldappwbyte.length - 20];
			System.arraycopy(ldappwbyte, 0, shacode, 0, 20);
			System.arraycopy(ldappwbyte, 20, salt, 0, salt.length);
		}

		// 把用户输入的密码添加到摘要计算信息
		md.update(inputpw.getBytes());
		// 把随机明文添加到摘要计算信息
		md.update(salt);

		// 按SSHA把当前用户密码进行计算
		byte[] inputpwbyte = md.digest();

		// 返回校验结果
		return MessageDigest.isEqual(shacode, inputpwbyte);
	}
}
